package xml;

import static javax.xml.stream.XMLStreamConstants.CDATA;
import static javax.xml.stream.XMLStreamConstants.CHARACTERS;
import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT;
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
import static javax.xml.stream.XMLStreamConstants.ENTITY_REFERENCE;
import static javax.xml.stream.XMLStreamConstants.SPACE;
import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT;
import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.SequenceInputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.xml.sax.SAXException;

public class Transformer {

	public static void main(String[] args) throws IOException,
			ParserConfigurationException, SAXException, XMLStreamException {
		String xml = "FullOct2007.xml";
		if (args.length > 0) {
			xml = args[0];
		}
		if (!new File(xml).exists()) {
			System.err.println("file not found");
			System.exit(1);
		}

		String out = "outqa/";
		if (args.length > 1) {
			out = args[1];
		}
		File outd = new File(out);
		if (!outd.canWrite()) {
			System.err.println("output directory can not write: " + out);
			System.exit(1);
		}

		perf.put(1, measure(1, xml, out));
		int minTn = 1;
		if (args.length > 2) {
			minTn = Integer.parseInt(args[2]);
		}
		int maxTn = Runtime.getRuntime().availableProcessors() * 10;
		if (args.length > 3) {
			maxTn = Integer.parseInt(args[3]);
		}
		perf.put(maxTn, measure(maxTn, xml, out));

		int bestTN = searchBestTN(minTn, maxTn, xml, out);

		try (Writer writer = new FileWriter(out + "/perf.txt");) {
			for (Map.Entry<Integer, Long> e : perf.entrySet()) {
				writer.write("tn=" + e.getKey() + ": " + e.getValue() + "ms\n");
			}
			writer.write("best=" + bestTN + ": " + perf.get(bestTN) + "ms\n");
		}
	}

	static Map<Integer, Long>	perf	= new HashMap<>();

	static int searchBestTN(int a, int b, String xml, String out) {
		if (a == b)
			return a;
		int tn = (a + b) / 2;
		long m = measure(tn, xml, out);
		perf.put(tn, m);
		long ma = perf.get(a);
		long mb = perf.get(b);
		if (ma > m && m > mb) {
			return searchBestTN(tn, b, xml, out);
		} else if (ma < m && m < mb) {
			return searchBestTN(a, tn, xml, out);
		} else if (ma < mb) {
			return searchBestTN(a, tn, xml, out);
		} else {
			return searchBestTN(tn, b, xml, out);
		}
	}

	static long measure(int tn, String xml, String out) {
		Transformer tran = new Transformer();
		tran.setThreadNumber(tn);
		tran.setXmlFile(new File(xml));
		tran.setOutputDirectory(new File(out));
		long s = System.currentTimeMillis();
		try {
			tran.run();
		} catch (ParserConfigurationException e1) {
			e1.printStackTrace();
		} catch (SAXException e1) {
			e1.printStackTrace();
		} catch (IOException e1) {
			e1.printStackTrace();
		} catch (XMLStreamException e1) {
			e1.printStackTrace();
		}
		long e = System.currentTimeMillis();

		return e - s;
	}

	public Transformer() {

	}

	int	threadNumber	= Runtime.getRuntime().availableProcessors() * 2;

	public void setThreadNumber(int n) {
		this.threadNumber = n;
	}

	File	xmlFile;

	File	outputDirectory;


	public File getOutputDirectory() {
		return outputDirectory;
	}



	public void setOutputDirectory(File outputDirectory) {
		this.outputDirectory = outputDirectory;
	}


	public File getXmlFile() {
		return xmlFile;
	}


	public void setXmlFile(File xmlFile) {
		this.xmlFile = xmlFile;
	}

	public void run() throws ParserConfigurationException, SAXException,
			IOException, XMLStreamException {

		long totalSize = xmlFile.length();
		totalSize=Long.min(totalSize, 50*1024*1024);
		long initialWorkSize = totalSize / threadNumber;

		List<TransformerTask> tasks = new ArrayList<>(threadNumber);

		// the last task
		TransformerTask lastTask = new TransformerTask(xmlFile,
				(threadNumber - 1) * initialWorkSize, totalSize
						- (threadNumber - 1) * initialWorkSize);
		lastTask.setOutputStream(new FileOutputStream(new File(outputDirectory
				+ "/" + (threadNumber - 1) + ".txt")));
		tasks.add(lastTask);

		for (int i = threadNumber - 2; i >= 0; i--) {
			TransformerTask task = new TransformerTask(xmlFile, i
					* initialWorkSize, initialWorkSize);
			task.setNextWorker(tasks.get(0));
			task.setOutputStream(new FileOutputStream(new File(outputDirectory
					+ "/" + i + ".txt")));
			tasks.add(0, task);
		}

		for (TransformerTask task : tasks) {
			task.getReady();
		}

		ExecutorService threadPool = Executors.newFixedThreadPool(threadNumber);

		for (TransformerTask task : tasks) {
			threadPool.submit(task);
		}

		threadPool.shutdown();
		System.out.println(Thread.currentThread().getName() + ": done");
	}
}


class TransformerTask implements Runnable {

	// SAXParser parser;
	// InputStream inputStream;
	long			workOffset;
	long			workSize;
	OutputStream	outputStream;

	long			firstRepeatingMostElementOffset	= -2;
	File			xmlFile;
	long			initialOffset;
	long			initialWorkSize;

	TransformerTask	previousWorker;
	TransformerTask	nextWorker;


	public TransformerTask getNextWorker() {
		return nextWorker;
	}


	public void setNextWorker(TransformerTask nextWorker) {
		this.nextWorker = nextWorker;
	}

	public OutputStream getOutputStream() {
		return outputStream;
	}



	public void setOutputStream(OutputStream outputStream) {
		this.outputStream = outputStream;
	}

	public TransformerTask(File xmlFile, long initialOffset,
			long initialWorkSize) throws IOException {
		this.xmlFile = xmlFile;
		this.initialOffset = initialOffset;
		this.initialWorkSize = initialWorkSize;
	}

	long getFirstRepeatingMostElementOffset() {
		if (firstRepeatingMostElementOffset < 0) {
			try (FileInputStream xmlInputStream = new FileInputStream(xmlFile);) {
				xmlInputStream.skip(initialOffset);
				firstRepeatingMostElementOffset = firstREB();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		return firstRepeatingMostElementOffset;
	}

	long	fRMEONextWorker	= -1;

	long getfRMEONextWorker() {
		if (fRMEONextWorker < 0) {
			fRMEONextWorker = (nextWorker == null) ? 0 : nextWorker
					.getFirstRepeatingMostElementOffset();
		}
		return fRMEONextWorker;
	}

	public void getReady() {
		workOffset = initialOffset + getFirstRepeatingMostElementOffset();

		workSize = initialWorkSize - getFirstRepeatingMostElementOffset()
				+ getfRMEONextWorker();

		// System.out.println("workoffset: " + workOffset + " nextoffset: "
		// + getfRMEONextWorker() + " worksize: " + workSize);

	}

	@Override
	public void run() {
		List<InputStream> isSrc = new ArrayList<>();
		isSrc.add(new ByteArrayInputStream(fakeRootOpenTag.getBytes()));
		try {
			isSrc.add(new LimitedInputStream(new FileInputStream(xmlFile),
					workOffset, workSize));

			isSrc.add(new ByteArrayInputStream(fakeRootCloseTag.getBytes()));

			XMLInputFactory factory = XMLInputFactory.newInstance();

			try (InputStream inputStream = new SequenceInputStream(
					Collections.enumeration(isSrc));
					Writer writer = new OutputStreamWriter(outputStream);) {

				XMLStreamReader xmlStreamReader = factory
						.createXMLStreamReader(inputStream);

				QA qa = new QA();
				String enclosingElement = "";
				String enclosingString = "";
				List<String> cAns = new ArrayList<>();
				qa.setCandidateAnswers(cAns);

				while (xmlStreamReader.hasNext()) {
					switch (xmlStreamReader.next()) {
					case START_DOCUMENT:

						break;

					case START_ELEMENT:
						String tag = xmlStreamReader.getLocalName();
						enclosingElement = tag;
						enclosingString = "";

						switch (tag) {
						case "document":
							cAns.clear();
							break;

						}

						// System.out.println("start ele: "+tag);
						break;

					case CHARACTERS:
					case CDATA:
						enclosingString += xmlStreamReader.getText();
						break;

					case SPACE:

						break;

					case ENTITY_REFERENCE:
						break;

					case END_ELEMENT:
						String endTag = xmlStreamReader.getLocalName();

						// System.out.println("end ele: " + endTag + "  "
						// + "enclos str: " + enclosingString);

						enclosingElement = "";

						switch (endTag) {
						case "subject":
							qa.setTitle(enclosingString);
							break;

						case "content":
							qa.setBody(enclosingString);
							break;

						case "bestanswer":
							qa.setBestAnswer(enclosingString);
							break;

						case "answer_item":
							if (!enclosingString.equals(qa.getBestAnswer())) {
								cAns.add(enclosingString);
							}
							break;

						case "cat":
							qa.setCategory(enclosingString);
							break;

						case "document":
							writer.write(qa.toString() + "\n");
							// System.out.println("entity:" + qa);
							break;
						}

						break;

					case END_DOCUMENT:
						break;

					default:
						// ignore
						break;
					}
				}

			} catch (XMLStreamException e) {
				e.printStackTrace();
			}

		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}

	// find the first repeating-most element open tag
	static String	repeatingMostTagName	= "vespaadd";

	long firstREB() throws IOException {
		long readCounter = 0;
		int c;
		int rc;
		byte[] buf = new byte[repeatingMostTagName.getBytes().length + 2];

		try (InputStream inputStream = new FileInputStream(xmlFile)) {
			inputStream.skip(initialOffset);
			long size = initialWorkSize;
			while (readCounter < size && (c = inputStream.read()) > 0) {
				readCounter++;
				if ('<' == c) {
					rc = inputStream.read(buf);
					readCounter += rc;

					if (repeatingMostTagName.equals(new String(buf, 0, rc)
							.split("\\s|>", 2)[0])) { // found
						readCounter -= rc + 1; // back to tag begin
						break;
					}
				}
			}
		}
		return readCounter;
	}

	// long findREEForward(InputStream inputStream, long initialWorkPosition,
	// long initialWorkSize) throws IOException {
	// long readCounter = 0;
	// int c;
	// int rc;
	// byte[] buf = new byte[repeatingMostTagName.getBytes().length + 2];
	// inputStream.skip(initialWorkPosition+initialWorkSize);
	// while ((c = inputStream.read()) > 0) {
	// readCounter++;
	// if ('<' == c) {
	// if ((c = inputStream.read()) > 0) {
	// readCounter++;
	// if ('/' == c) {
	// rc = inputStream.read(buf);
	// readCounter += rc;
	// if (repeatingMostTagName.equals(new String(buf, 0, rc)
	// .split("\\s|>", 2)[0])) { // found
	// break;
	// }
	// }
	// }
	// }
	// }
	// return readCounter;
	// }

	static String	fakeRootOpenTag		= "<root>";
	static String	fakeRootCloseTag	= "</root>";


}


class LimitedInputStream extends InputStream {

	InputStream	inputStream;
	long		offset;
	long		limitSize;
	long		remaining;

	public LimitedInputStream(InputStream inputStream, long offset,
			long limitSize) throws IOException {
		super();
		this.inputStream = inputStream;
		this.offset = offset;
		this.limitSize = limitSize;
		remaining = limitSize;
		// System.out.println("offset: "+offset+" size: "+limitSize);
		inputStream.skip(offset);

	}

	@Override
	public int available() throws IOException {
		return remaining > Integer.MAX_VALUE ? Integer.MAX_VALUE
				: (int) remaining;
	}

	@Override
	public int read() throws IOException {
		// if (remaining > 0) {
		// remaining--;
		// return inputStream.read();
		// } else if (remaining == 0) {
		// remaining--;
		// return -1;
		// } else
		// return -1;
		return (remaining-- <= 0) ? -1 : inputStream.read();
	}

	@Override
	public void close() throws IOException {
		inputStream.close();
	}
}


class QA {

	String			title;
	String			body;
	String			bestAnswer;
	String			category;
	List<String>	candidateAnswers;


	public List<String> getCandidateAnswers() {
		return candidateAnswers;
	}


	public void setCandidateAnswers(List<String> candidateAnswers) {
		this.candidateAnswers = candidateAnswers;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getBody() {
		return body;
	}

	public void setBody(String body) {
		this.body = body;
	}

	public String getBestAnswer() {
		return bestAnswer;
	}

	public void setBestAnswer(String answer) {
		this.bestAnswer = answer;
	}

	public String getCategory() {
		return category;
	}

	public void setCategory(String category) {
		this.category = category;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder(title + "\t" + body + "\t"
				+ bestAnswer + "\t1\t" + category);
		for (String ca : candidateAnswers) {
			sb.append("\n" + title + "\t" + body + "\t" + ca + "\t0\t"
					+ category);
		}
		return sb.toString();
	}
}
